﻿using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using static Win32API.WinApi;

namespace Win32API
{
    /// <summary>
    /// 键盘钩子
    /// </summary>
    public class KeyboardHook : IDisposable
    {
        /// <summary>
        /// 键盘组合键同时按下
        /// </summary>
        public event KeyEventHandler KeyDownEvent;
        /// <summary>
        /// 键盘按住
        /// </summary>
        public event KeyPressEventHandler KeyPressEvent;
        /// <summary>
        /// 键盘弹起
        /// </summary>
        public event KeyEventHandler KeyUpEvent;

        static int _hHook = 0; //声明键盘钩子处理的初始值

        /// <summary>
        /// 钩子回调
        /// </summary>
        /// <remarks>
        /// 不要试图省略此变量,否则将会导致
        /// 激活 CallbackOnCollectedDelegate 托管调试助手(MDA) 
        /// 详细 https://www.cnblogs.com/lclblog/p/4620997.html
        /// </remarks>
        HookProc _hookProc;


        //https://blog.csdn.net/u014434080/article/details/51513197
        /// <summary>
        /// 安装键盘钩子
        /// </summary>
        /// <param name="IsSystemHook">系统钩子true,线程钩子false</param>
        public KeyboardHook(bool IsSystemHook = true)
        {
            if (_hHook != 0)
            {
                return;
            }

            //回调函数
            _hookProc = new HookProc(KeyboardHookProc);

            if (IsSystemHook)
            {
                //系统钩子
                var moduleName = GetModuleHandle(System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName);
                _hHook = SetWindowsHookEx(HookType.WH_KEYBOARD_LL, _hookProc, moduleName, 0);
            }
            else
            {
                //线程钩子(cad08用会出错,见回调函数说明)
                _hHook = SetWindowsHookEx(HookType.WH_KEYBOARD, _hookProc, IntPtr.Zero, GetCurrentThreadId());
            }

            //如果SetWindowsHookEx失败
            if (_hHook == 0)
            {
                Stop();
                throw new Exception("安装键盘钩子失败");
            }
        }
         
        /// <summary>
        /// 中断回调阻断输入
        /// </summary>
        public bool IsHookBreak = false;

        /// <summary>
        /// 回调函数
        /// </summary>     
        private int KeyboardHookProc(int nCode, int wParam, IntPtr lParam)
        {
            IsHookBreak = false;
            // 侦听键盘事件
            if ((nCode >= 0) && (KeyDownEvent != null || KeyUpEvent != null || KeyPressEvent != null))
            {
                var wPa = (WM)wParam;

#if true333
                //cad08线程钩子转换键值会变成以下的值,没弄懂..
                //所以我用系统钩子再判断窗口焦点是cad才执行.GetForegroundWindow();

                if (wPa == WM.WM_QUERYENDSESSION)//按ctrl会传这个信息
                {
                    return CallNextHookEx(_hHook, nCode, wParam, lParam);
                } 
#endif

                var keyMSG = (KeyMSG)Marshal.PtrToStructure(lParam, typeof(KeyMSG));

                //键盘组合键同时按下
                if (KeyDownEvent != null && (wPa == WM.WM_KEYDOWN || wPa == WM.WM_SYSKEYDOWN))
                {
                    Keys keyData = (Keys)keyMSG.vkCode;
                    KeyEventArgs e = new KeyEventArgs(keyData);
                    KeyDownEvent(this, e);
                }

                //键盘按下
                if (KeyPressEvent != null && wPa == WM.WM_KEYDOWN)
                {
                    byte[] keyState = new byte[256];
                    GetKeyboardState(keyState);

                    byte[] inBuffer = new byte[2];
                    if (ToAscii(keyMSG.vkCode, keyMSG.scanCode, keyState, inBuffer, keyMSG.flags) == 1)
                    {
                        KeyPressEventArgs e = new KeyPressEventArgs((char)inBuffer[0]);
                        KeyPressEvent(this, e);
                    }
                }

                // 键盘抬起
                if (KeyUpEvent != null && (wPa == WM.WM_KEYUP || wPa == WM.WM_SYSKEYUP))
                {
                    Keys keyData = (Keys)keyMSG.vkCode;
                    KeyEventArgs e = new KeyEventArgs(keyData);
                    KeyUpEvent(this, e);
                }

                if (IsHookBreak)
                {
                    return 1;
                }
            }
            //如果返回1,不再传递。
            //如果返回0或调用CallNextHookEx函数则消息出了这个钩子继续往下传递，也就是传给消息真正的接受者
            return CallNextHookEx(_hHook, nCode, wParam, lParam);
        }



        void Stop()
        {
            bool retKeyboard = true;

            if (_hHook != 0)
            {
                retKeyboard = UnhookWindowsHookEx(_hHook);
                _hHook = 0;
            }

            if (!retKeyboard) throw new Exception("卸载钩子失败！");
        }


        #region Dispose
        public bool Disposed = false;

        /// <summary>
        /// 显式调用Dispose方法,继承IDisposable
        /// </summary>
        public void Dispose()
        {
            //由手动释放
            Dispose(true);
            //通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// 析构函数,以备忘记了显式调用Dispose方法
        /// </summary> 
        ~KeyboardHook()
        {
            //由系统释放
            Dispose(false);
        }

        //https://kb.cnblogs.com/page/97871/
        /// <summary>
        /// 释放
        /// </summary>
        /// <param name="ing"></param>
        protected virtual void Dispose(bool ing)
        {
            if (Disposed)
            {
                //不重复释放
                return;
            }
            //让类型知道自己已经被释放
            Disposed = true;

            Stop();
        }
        #endregion
    }
}